<?php
/**
 * Extension argument processing code
 *
 * @package OpenID
 */

/**
 * Import tools needed to deal with messages.
 */
require_once 'Auth/OpenID.php';
require_once 'Auth/OpenID/KVForm.php';
require_once 'Auth/Yadis/XML.php';
require_once 'Auth/OpenID/Consumer.php'; // For Auth_OpenID_FailureResponse


// This doesn't REALLY belong here, but where is better?
define('Auth_OpenID_IDENTIFIER_SELECT', "http://specs.openid.net/auth/2.0/identifier_select");

// URI for Simple Registration extension, the only commonly deployed
// OpenID 1.x extension, and so a special case
define('Auth_OpenID_SREG_URI', 'http://openid.net/sreg/1.0');

// The OpenID 1.X namespace URI
define('Auth_OpenID_OPENID1_NS', 'http://openid.net/signon/1.0');
define('Auth_OpenID_THE_OTHER_OPENID1_NS', 'http://openid.net/signon/1.1');

function Auth_OpenID_isOpenID1($ns) {
  return ($ns == Auth_OpenID_THE_OTHER_OPENID1_NS) || ($ns == Auth_OpenID_OPENID1_NS);
}

// The OpenID 2.0 namespace URI
define('Auth_OpenID_OPENID2_NS', 'http://specs.openid.net/auth/2.0');

// The namespace consisting of pairs with keys that are prefixed with
// "openid."  but not in another namespace.
define('Auth_OpenID_NULL_NAMESPACE', 'Null namespace');

// The null namespace, when it is an allowed OpenID namespace
define('Auth_OpenID_OPENID_NS', 'OpenID namespace');

// The top-level namespace, excluding all pairs with keys that start
// with "openid."
define('Auth_OpenID_BARE_NS', 'Bare namespace');

// Sentinel for Message implementation to indicate that getArg should
// return null instead of returning a default.
define('Auth_OpenID_NO_DEFAULT', 'NO DEFAULT ALLOWED');

// Limit, in bytes, of identity provider and return_to URLs, including
// response payload.  See OpenID 1.1 specification, Appendix D.
define('Auth_OpenID_OPENID1_URL_LIMIT', 2047);

// All OpenID protocol fields.  Used to check namespace aliases.
global $Auth_OpenID_OPENID_PROTOCOL_FIELDS;
$Auth_OpenID_OPENID_PROTOCOL_FIELDS = array('ns', 'mode', 'error', 'return_to', 'contact', 'reference', 'signed', 
    'assoc_type', 'session_type', 'dh_modulus', 'dh_gen', 'dh_consumer_public', 'claimed_id', 'identity', 
    'realm', 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig', 'assoc_handle', 'trust_root', 
    'openid');

// Global namespace / alias registration map.  See
// Auth_OpenID_registerNamespaceAlias.
global $Auth_OpenID_registered_aliases;
$Auth_OpenID_registered_aliases = array();

/**
 * Registers a (namespace URI, alias) mapping in a global namespace
 * alias map.  Raises NamespaceAliasRegistrationError if either the
 * namespace URI or alias has already been registered with a different
 * value.  This function is required if you want to use a namespace
 * with an OpenID 1 message.
 */
function Auth_OpenID_registerNamespaceAlias($namespace_uri, $alias) {
  global $Auth_OpenID_registered_aliases;
  
  if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, $alias) == $namespace_uri) {
    return true;
  }
  
  if (in_array($namespace_uri, array_values($Auth_OpenID_registered_aliases))) {
    return false;
  }
  
  if (in_array($alias, array_keys($Auth_OpenID_registered_aliases))) {
    return false;
  }
  
  $Auth_OpenID_registered_aliases[$alias] = $namespace_uri;
  return true;
}

/**
 * Removes a (namespace_uri, alias) registration from the global
 * namespace alias map.  Returns true if the removal succeeded; false
 * if not (if the mapping did not exist).
 */
function Auth_OpenID_removeNamespaceAlias($namespace_uri, $alias) {
  global $Auth_OpenID_registered_aliases;
  
  if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, $alias) === $namespace_uri) {
    unset($Auth_OpenID_registered_aliases[$alias]);
    return true;
  }
  
  return false;
}

/**
 * An Auth_OpenID_Mapping maintains a mapping from arbitrary keys to
 * arbitrary values.  (This is unlike an ordinary PHP array, whose
 * keys may be only simple scalars.)
 *
 * @package OpenID
 */
class Auth_OpenID_Mapping {

  /**
   * Initialize a mapping.  If $classic_array is specified, its keys
   * and values are used to populate the mapping.
   */
  function Auth_OpenID_Mapping($classic_array = null) {
    $this->keys = array();
    $this->values = array();
    
    if (is_array($classic_array)) {
      foreach ($classic_array as $key => $value) {
        $this->set($key, $value);
      }
    }
  }

  /**
   * Returns true if $thing is an Auth_OpenID_Mapping object; false
   * if not.
   */
  function isA($thing) {
    return (is_object($thing) && strtolower(get_class($thing)) == 'auth_openid_mapping');
  }

  /**
   * Returns an array of the keys in the mapping.
   */
  function keys() {
    return $this->keys;
  }

  /**
   * Returns an array of values in the mapping.
   */
  function values() {
    return $this->values;
  }

  /**
   * Returns an array of (key, value) pairs in the mapping.
   */
  function items() {
    $temp = array();
    
    for ($i = 0; $i < count($this->keys); $i ++) {
      $temp[] = array($this->keys[$i], $this->values[$i]);
    }
    return $temp;
  }

  /**
   * Returns the "length" of the mapping, or the number of keys.
   */
  function len() {
    return count($this->keys);
  }

  /**
   * Sets a key-value pair in the mapping.  If the key already
   * exists, its value is replaced with the new value.
   */
  function set($key, $value) {
    $index = array_search($key, $this->keys);
    
    if ($index !== false) {
      $this->values[$index] = $value;
    } else {
      $this->keys[] = $key;
      $this->values[] = $value;
    }
  }

  /**
   * Gets a specified value from the mapping, associated with the
   * specified key.  If the key does not exist in the mapping,
   * $default is returned instead.
   */
  function get($key, $default = null) {
    $index = array_search($key, $this->keys);
    
    if ($index !== false) {
      return $this->values[$index];
    } else {
      return $default;
    }
  }

  /**
   * @access private
   */
  function _reflow() {
    // PHP is broken yet again.  Sort the arrays to remove the
    // hole in the numeric indexes that make up the array.
    $old_keys = $this->keys;
    $old_values = $this->values;
    
    $this->keys = array();
    $this->values = array();
    
    foreach ($old_keys as $k) {
      $this->keys[] = $k;
    }
    
    foreach ($old_values as $v) {
      $this->values[] = $v;
    }
  }

  /**
   * Deletes a key-value pair from the mapping with the specified
   * key.
   */
  function del($key) {
    $index = array_search($key, $this->keys);
    
    if ($index !== false) {
      unset($this->keys[$index]);
      unset($this->values[$index]);
      $this->_reflow();
      return true;
    }
    return false;
  }

  /**
   * Returns true if the specified value has a key in the mapping;
   * false if not.
   */
  function contains($value) {
    return (array_search($value, $this->keys) !== false);
  }
}

/**
 * Maintains a bijective map between namespace uris and aliases.
 *
 * @package OpenID
 */
class Auth_OpenID_NamespaceMap {

  function Auth_OpenID_NamespaceMap() {
    $this->alias_to_namespace = new Auth_OpenID_Mapping();
    $this->namespace_to_alias = new Auth_OpenID_Mapping();
    $this->implicit_namespaces = array();
  }

  function getAlias($namespace_uri) {
    return $this->namespace_to_alias->get($namespace_uri);
  }

  function getNamespaceURI($alias) {
    return $this->alias_to_namespace->get($alias);
  }

  function iterNamespaceURIs() {
    // Return an iterator over the namespace URIs
    return $this->namespace_to_alias->keys();
  }

  function iterAliases() {
    // Return an iterator over the aliases"""
    return $this->alias_to_namespace->keys();
  }

  function iteritems() {
    return $this->namespace_to_alias->items();
  }

  function isImplicit($namespace_uri) {
    return in_array($namespace_uri, $this->implicit_namespaces);
  }

  function addAlias($namespace_uri, $desired_alias, $implicit = false) {
    // Add an alias from this namespace URI to the desired alias
    global $Auth_OpenID_OPENID_PROTOCOL_FIELDS;
    
    // Check that desired_alias is not an openid protocol field as
    // per the spec.
    if (in_array($desired_alias, $Auth_OpenID_OPENID_PROTOCOL_FIELDS)) {
      Auth_OpenID::log("\"%s\" is not an allowed namespace alias", $desired_alias);
      return null;
    }
    
    // Check that desired_alias does not contain a period as per
    // the spec.
    if (strpos($desired_alias, '.') !== false) {
      Auth_OpenID::log('"%s" must not contain a dot', $desired_alias);
      return null;
    }
    
    // Check that there is not a namespace already defined for the
    // desired alias
    $current_namespace_uri = $this->alias_to_namespace->get($desired_alias);
    
    if (($current_namespace_uri !== null) && ($current_namespace_uri != $namespace_uri)) {
      Auth_OpenID::log('Cannot map "%s" because previous mapping exists', $namespace_uri);
      return null;
    }
    
    // Check that there is not already a (different) alias for
    // this namespace URI
    $alias = $this->namespace_to_alias->get($namespace_uri);
    
    if (($alias !== null) && ($alias != $desired_alias)) {
      Auth_OpenID::log('Cannot map %s to alias %s. ' . 'It is already mapped to alias %s', $namespace_uri, $desired_alias, $alias);
      return null;
    }
    
    assert((Auth_OpenID_NULL_NAMESPACE === $desired_alias) || is_string($desired_alias));
    
    $this->alias_to_namespace->set($desired_alias, $namespace_uri);
    $this->namespace_to_alias->set($namespace_uri, $desired_alias);
    if ($implicit) {
      array_push($this->implicit_namespaces, $namespace_uri);
    }
    
    return $desired_alias;
  }

  function add($namespace_uri) {
    // Add this namespace URI to the mapping, without caring what
    // alias it ends up with
    

    // See if this namespace is already mapped to an alias
    $alias = $this->namespace_to_alias->get($namespace_uri);
    
    if ($alias !== null) {
      return $alias;
    }
    
    // Fall back to generating a numerical alias
    $i = 0;
    while (1) {
      $alias = 'ext' . strval($i);
      if ($this->addAlias($namespace_uri, $alias) === null) {
        $i += 1;
      } else {
        return $alias;
      }
    }
    
    // Should NEVER be reached!
    return null;
  }

  function contains($namespace_uri) {
    return $this->isDefined($namespace_uri);
  }

  function isDefined($namespace_uri) {
    return $this->namespace_to_alias->contains($namespace_uri);
  }
}

/**
 * In the implementation of this object, null represents the global
 * namespace as well as a namespace with no key.
 *
 * @package OpenID
 */
class Auth_OpenID_Message {

  function Auth_OpenID_Message($openid_namespace = null) {
    // Create an empty Message
    $this->allowed_openid_namespaces = array(Auth_OpenID_OPENID1_NS, 
        Auth_OpenID_THE_OTHER_OPENID1_NS, Auth_OpenID_OPENID2_NS);
    
    $this->args = new Auth_OpenID_Mapping();
    $this->namespaces = new Auth_OpenID_NamespaceMap();
    if ($openid_namespace === null) {
      $this->_openid_ns_uri = null;
    } else {
      $implicit = Auth_OpenID_isOpenID1($openid_namespace);
      $this->setOpenIDNamespace($openid_namespace, $implicit);
    }
  }

  function isOpenID1() {
    return Auth_OpenID_isOpenID1($this->getOpenIDNamespace());
  }

  function isOpenID2() {
    return $this->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS;
  }

  function fromPostArgs($args) {
    // Construct a Message containing a set of POST arguments
    $obj = new Auth_OpenID_Message();
    
    // Partition into "openid." args and bare args
    $openid_args = array();
    foreach ($args as $key => $value) {
      
      if (is_array($value)) {
        return null;
      }
      
      $parts = explode('.', $key, 2);
      
      if (count($parts) == 2) {
        list($prefix, $rest) = $parts;
      } else {
        $prefix = null;
      }
      
      if ($prefix != 'openid') {
        $obj->args->set(array(Auth_OpenID_BARE_NS, $key), $value);
      } else {
        $openid_args[$rest] = $value;
      }
    }
    
    if ($obj->_fromOpenIDArgs($openid_args)) {
      return $obj;
    } else {
      return null;
    }
  }

  function fromOpenIDArgs($openid_args) {
    // Takes an array.
    

    // Construct a Message from a parsed KVForm message
    $obj = new Auth_OpenID_Message();
    if ($obj->_fromOpenIDArgs($openid_args)) {
      return $obj;
    } else {
      return null;
    }
  }

  /**
   * @access private
   */
  function _fromOpenIDArgs($openid_args) {
    global $Auth_OpenID_registered_aliases;
    
    // Takes an Auth_OpenID_Mapping instance OR an array.
    

    if (! Auth_OpenID_Mapping::isA($openid_args)) {
      $openid_args = new Auth_OpenID_Mapping($openid_args);
    }
    
    $ns_args = array();
    
    // Resolve namespaces
    foreach ($openid_args->items() as $pair) {
      list($rest, $value) = $pair;
      
      $parts = explode('.', $rest, 2);
      
      if (count($parts) == 2) {
        list($ns_alias, $ns_key) = $parts;
      } else {
        $ns_alias = Auth_OpenID_NULL_NAMESPACE;
        $ns_key = $rest;
      }
      
      if ($ns_alias == 'ns') {
        if ($this->namespaces->addAlias($value, $ns_key) === null) {
          return false;
        }
      } else if (($ns_alias == Auth_OpenID_NULL_NAMESPACE) && ($ns_key == 'ns')) {
        // null namespace
        if ($this->setOpenIDNamespace($value, false) === false) {
          return false;
        }
      } else {
        $ns_args[] = array($ns_alias, $ns_key, $value);
      }
    }
    
    if (! $this->getOpenIDNamespace()) {
      if ($this->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, true) === false) {
        return false;
      }
    }
    
    // Actually put the pairs into the appropriate namespaces
    foreach ($ns_args as $triple) {
      list($ns_alias, $ns_key, $value) = $triple;
      $ns_uri = $this->namespaces->getNamespaceURI($ns_alias);
      if ($ns_uri === null) {
        $ns_uri = $this->_getDefaultNamespace($ns_alias);
        if ($ns_uri === null) {
          
          $ns_uri = Auth_OpenID_OPENID_NS;
          $ns_key = sprintf('%s.%s', $ns_alias, $ns_key);
        } else {
          $this->namespaces->addAlias($ns_uri, $ns_alias, true);
        }
      }
      
      $this->setArg($ns_uri, $ns_key, $value);
    }
    
    return true;
  }

  function _getDefaultNamespace($mystery_alias) {
    global $Auth_OpenID_registered_aliases;
    if ($this->isOpenID1()) {
      return @$Auth_OpenID_registered_aliases[$mystery_alias];
    }
    return null;
  }

  function setOpenIDNamespace($openid_ns_uri, $implicit) {
    if (! in_array($openid_ns_uri, $this->allowed_openid_namespaces)) {
      Auth_OpenID::log('Invalid null namespace: "%s"', $openid_ns_uri);
      return false;
    }
    
    $succeeded = $this->namespaces->addAlias($openid_ns_uri, Auth_OpenID_NULL_NAMESPACE, $implicit);
    if ($succeeded === false) {
      return false;
    }
    
    $this->_openid_ns_uri = $openid_ns_uri;
    
    return true;
  }

  function getOpenIDNamespace() {
    return $this->_openid_ns_uri;
  }

  function fromKVForm($kvform_string) {
    // Create a Message from a KVForm string
    return Auth_OpenID_Message::fromOpenIDArgs(Auth_OpenID_KVForm::toArray($kvform_string));
  }

  function copy() {
    return $this;
  }

  function toPostArgs() {
    // Return all arguments with openid. in front of namespaced
    // arguments.
    

    $args = array();
    
    // Add namespace definitions to the output
    foreach ($this->namespaces->iteritems() as $pair) {
      list($ns_uri, $alias) = $pair;
      if ($this->namespaces->isImplicit($ns_uri)) {
        continue;
      }
      if ($alias == Auth_OpenID_NULL_NAMESPACE) {
        $ns_key = 'openid.ns';
      } else {
        $ns_key = 'openid.ns.' . $alias;
      }
      $args[$ns_key] = $ns_uri;
    }
    
    foreach ($this->args->items() as $pair) {
      list($ns_parts, $value) = $pair;
      list($ns_uri, $ns_key) = $ns_parts;
      $key = $this->getKey($ns_uri, $ns_key);
      $args[$key] = $value;
    }
    
    return $args;
  }

  function toArgs() {
    // Return all namespaced arguments, failing if any
    // non-namespaced arguments exist.
    $post_args = $this->toPostArgs();
    $kvargs = array();
    foreach ($post_args as $k => $v) {
      if (strpos($k, 'openid.') !== 0) {
        // raise ValueError(
        //   'This message can only be encoded as a POST, because it '
        //   'contains arguments that are not prefixed with "openid."')
        return null;
      } else {
        $kvargs[substr($k, 7)] = $v;
      }
    }
    
    return $kvargs;
  }

  function toFormMarkup($action_url, $form_tag_attrs = null, $submit_text = "Continue") {
    $form = "<form accept-charset=\"UTF-8\" " . "enctype=\"application/x-www-form-urlencoded\"";
    
    if (! $form_tag_attrs) {
      $form_tag_attrs = array();
    }
    
    $form_tag_attrs['action'] = $action_url;
    $form_tag_attrs['method'] = 'post';
    
    unset($form_tag_attrs['enctype']);
    unset($form_tag_attrs['accept-charset']);
    
    if ($form_tag_attrs) {
      foreach ($form_tag_attrs as $name => $attr) {
        $form .= sprintf(" %s=\"%s\"", $name, $attr);
      }
    }
    
    $form .= ">\n";
    
    foreach ($this->toPostArgs() as $name => $value) {
      $form .= sprintf("<input type=\"hidden\" name=\"%s\" value=\"%s\" />\n", $name, $value);
    }
    
    $form .= sprintf("<input type=\"submit\" value=\"%s\" />\n", $submit_text);
    
    $form .= "</form>\n";
    
    return $form;
  }

  function toURL($base_url) {
    // Generate a GET URL with the parameters in this message
    // attached as query parameters.
    return Auth_OpenID::appendArgs($base_url, $this->toPostArgs());
  }

  function toKVForm() {
    // Generate a KVForm string that contains the parameters in
    // this message. This will fail if the message contains
    // arguments outside of the 'openid.' prefix.
    return Auth_OpenID_KVForm::fromArray($this->toArgs());
  }

  function toURLEncoded() {
    // Generate an x-www-urlencoded string
    $args = array();
    
    foreach ($this->toPostArgs() as $k => $v) {
      $args[] = array($k, $v);
    }
    
    sort($args);
    return Auth_OpenID::httpBuildQuery($args);
  }

  /**
   * @access private
   */
  function _fixNS($namespace) {
    // Convert an input value into the internally used values of
    // this object
    

    if ($namespace == Auth_OpenID_OPENID_NS) {
      if ($this->_openid_ns_uri === null) {
        return new Auth_OpenID_FailureResponse(null, 'OpenID namespace not set');
      } else {
        $namespace = $this->_openid_ns_uri;
      }
    }
    
    if (($namespace != Auth_OpenID_BARE_NS) && (! is_string($namespace))) {
      //TypeError
      $err_msg = sprintf("Namespace must be Auth_OpenID_BARE_NS, " . "Auth_OpenID_OPENID_NS or a string. got %s", print_r($namespace, true));
      return new Auth_OpenID_FailureResponse(null, $err_msg);
    }
    
    if (($namespace != Auth_OpenID_BARE_NS) && (strpos($namespace, ':') === false)) {
      // fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r'
      // warnings.warn(fmt % (namespace,), DeprecationWarning)
      

      if ($namespace == 'sreg') {
        // fmt = 'Using %r instead of "sreg" as namespace'
        // warnings.warn(fmt % (SREG_URI,), DeprecationWarning,)
        return Auth_OpenID_SREG_URI;
      }
    }
    
    return $namespace;
  }

  function hasKey($namespace, $ns_key) {
    $namespace = $this->_fixNS($namespace);
    if (Auth_OpenID::isFailure($namespace)) {
      // XXX log me
      return false;
    } else {
      return $this->args->contains(array($namespace, $ns_key));
    }
  }

  function getKey($namespace, $ns_key) {
    // Get the key for a particular namespaced argument
    $namespace = $this->_fixNS($namespace);
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    }
    if ($namespace == Auth_OpenID_BARE_NS) {
      return $ns_key;
    }
    
    $ns_alias = $this->namespaces->getAlias($namespace);
    
    // No alias is defined, so no key can exist
    if ($ns_alias === null) {
      return null;
    }
    
    if ($ns_alias == Auth_OpenID_NULL_NAMESPACE) {
      $tail = $ns_key;
    } else {
      $tail = sprintf('%s.%s', $ns_alias, $ns_key);
    }
    
    return 'openid.' . $tail;
  }

  function getArg($namespace, $key, $default = null) {
    // Get a value for a namespaced key.
    $namespace = $this->_fixNS($namespace);
    
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    } else {
      if ((! $this->args->contains(array($namespace, $key))) && ($default == Auth_OpenID_NO_DEFAULT)) {
        $err_msg = sprintf("Namespace %s missing required field %s", $namespace, $key);
        return new Auth_OpenID_FailureResponse(null, $err_msg);
      } else {
        return $this->args->get(array($namespace, $key), $default);
      }
    }
  }

  function getArgs($namespace) {
    // Get the arguments that are defined for this namespace URI
    

    $namespace = $this->_fixNS($namespace);
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    } else {
      $stuff = array();
      foreach ($this->args->items() as $pair) {
        list($key, $value) = $pair;
        list($pair_ns, $ns_key) = $key;
        if ($pair_ns == $namespace) {
          $stuff[$ns_key] = $value;
        }
      }
      
      return $stuff;
    }
  }

  function updateArgs($namespace, $updates) {
    // Set multiple key/value pairs in one call
    

    $namespace = $this->_fixNS($namespace);
    
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    } else {
      foreach ($updates as $k => $v) {
        $this->setArg($namespace, $k, $v);
      }
      return true;
    }
  }

  function setArg($namespace, $key, $value) {
    // Set a single argument in this namespace
    $namespace = $this->_fixNS($namespace);
    
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    } else {
      $this->args->set(array($namespace, $key), $value);
      if ($namespace !== Auth_OpenID_BARE_NS) {
        $this->namespaces->add($namespace);
      }
      return true;
    }
  }

  function delArg($namespace, $key) {
    $namespace = $this->_fixNS($namespace);
    
    if (Auth_OpenID::isFailure($namespace)) {
      return $namespace;
    } else {
      return $this->args->del(array($namespace, $key));
    }
  }

  function getAliasedArg($aliased_key, $default = null) {
    $parts = explode('.', $aliased_key, 2);
    
    if (count($parts) != 2) {
      $ns = null;
    } else {
      list($alias, $key) = $parts;
      
      if ($alias == 'ns') {
        // Return the namespace URI for a namespace alias
        // parameter.
        return $this->namespaces->getNamespaceURI($key);
      } else {
        $ns = $this->namespaces->getNamespaceURI($alias);
      }
    }
    
    if ($ns === null) {
      $key = $aliased_key;
      $ns = $this->getOpenIDNamespace();
    }
    
    return $this->getArg($ns, $key, $default);
  }
}
